//
//  MKNetworkOperation.m
//  MKNetworkKit
//
//  Created by Mugunth Kumar (@mugunthkumar) on 11/11/11.
//  Copyright (C) 2011-2020 by Steinlogic Consulting and Training Pte Ltd

//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
#if kSupportMKNetworkKit
#import "MKNetworkKit.h"

#import <ImageIO/ImageIO.h>

#ifdef __OBJC_GC__
#error MKNetworkKit does not support Objective-C Garbage Collection
#endif

#if TARGET_OS_IPHONE
#ifndef __IPHONE_5_0
#error MKNetworkKit does not support iOS 4 and lower
#endif
#endif

#if ! __has_feature(objc_arc)
#error MKNetworkKit is ARC only. Either turn on ARC for the project or use -fobjc-arc flag
#endif

OSStatus extractIdentityAndTrust(CFDataRef inPKCS12Data,
                                 SecIdentityRef *outIdentity,
                                 SecTrustRef *outTrust,
                                 CFStringRef keyPassword);

@interface MKNetworkOperation (/*Private Methods*/)
@property (strong, nonatomic) NSURLConnection *connection;
@property (copy, nonatomic) NSString *uniqueId;
@property (strong, nonatomic) NSMutableURLRequest *request;
@property (strong, nonatomic) NSHTTPURLResponse *response;

@property (strong, nonatomic) NSMutableDictionary *fieldsToBePosted;
@property (strong, nonatomic) NSMutableArray *filesToBePosted;
@property (strong, nonatomic) NSMutableArray *dataToBePosted;

@property (copy, nonatomic) NSString *username;
@property (copy, nonatomic) NSString *password;

@property (nonatomic, strong) NSMutableArray *responseBlocks;
@property (nonatomic, strong) NSMutableArray *errorBlocks;
@property (nonatomic, strong) NSMutableArray *errorBlocksType2;

@property (nonatomic, assign) MKNetworkOperationState state;
@property (nonatomic, assign) BOOL isCancelled;

@property (strong, nonatomic) NSMutableData *mutableData;
@property (assign, nonatomic) NSUInteger downloadedDataSize;

@property (nonatomic, strong) NSMutableArray *notModifiedHandlers;

@property (nonatomic, strong) NSMutableArray *uploadProgressChangedHandlers;
@property (nonatomic, strong) NSMutableArray *downloadProgressChangedHandlers;
@property (nonatomic, strong) NSMutableArray *segmentedDownloadChangedHandlers;
@property (nonatomic, copy) MKNKEncodingBlock postDataEncodingHandler;

@property (nonatomic, assign) NSInteger startPosition;

@property (nonatomic, strong) NSMutableArray *downloadStreams;
@property (nonatomic, copy) NSData *cachedResponse;
@property (nonatomic, copy) MKNKResponseBlock cacheHandlingBlock;

@property (nonatomic, assign) SecTrustRef serverTrust;

#if TARGET_OS_IPHONE
@property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskId;
#endif

@property (strong, nonatomic) NSError *error;

- (instancetype)initWithURLString:(NSString *)aURLString
                           params:(NSDictionary *)body
                       httpMethod:(NSString *)method;

-(NSData*) bodyData;

-(NSString*) encodedPostDataString;
- (void) showLocalNotification;
- (void) endBackgroundTask;

@end

@implementation MKNetworkOperation

@dynamic freezable;

// A RESTful service should always return the same response for a given URL and it's parameters.
// this means if these values are correct, you can cache the responses
// This is another reason why we check only GET methods.
// even if URL and others are same, POST, DELETE, PUT methods should not be cached and should not be treated equal.

-(BOOL) isCacheable {
    
    if(self.shouldNotCacheResponse) return NO;
    if(self.username != nil) return NO;
    if(self.password != nil) return NO;
    if(self.clientCertificate != nil) return NO;
    if(self.clientCertificatePassword != nil) return NO;
    if(![self.request.HTTPMethod isEqualToString:@"GET"]) return NO;
    if([self.request.URL.scheme.lowercaseString isEqualToString:@"https"]) return self.shouldCacheResponseEvenIfProtocolIsHTTPS;
    if(self.downloadStreams.count > 0) return NO; // should not cache operations that have streams attached
    return YES;
}


//===========================================================
// + (BOOL)automaticallyNotifiesObserversForKey:
//
//===========================================================
+ (BOOL)automaticallyNotifiesObserversForKey: (NSString *)theKey
{
    BOOL automatic;
    
    if ([theKey isEqualToString:@"postDataEncoding"]) {
        automatic = NO;
    } else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    
    return automatic;
}

//===========================================================
//  postDataEncoding
//===========================================================
- (MKNKPostDataEncodingType)postDataEncoding
{
    return _postDataEncoding;
}
- (void)setPostDataEncoding:(MKNKPostDataEncodingType)aPostDataEncoding
{
    _postDataEncoding = aPostDataEncoding;
    
    NSString *charset = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(self.stringEncoding));
    
    switch (self.postDataEncoding) {
            
        case MKNKPostDataEncodingTypeURL: {
            [self.request setValue:
             [NSString stringWithFormat:@"application/x-www-form-urlencoded; charset=%@", charset]
                forHTTPHeaderField:@"Content-Type"];
        }
            break;
        case MKNKPostDataEncodingTypeJSON: {
            [self.request setValue:
             [NSString stringWithFormat:@"application/json; charset=%@", charset]
                forHTTPHeaderField:@"Content-Type"];
        }
            break;
        case MKNKPostDataEncodingTypePlist: {
            [self.request setValue:
             [NSString stringWithFormat:@"application/x-plist; charset=%@", charset]
                forHTTPHeaderField:@"Content-Type"];
        }
            
        default:
            break;
    }
}

-(NSString*) encodedPostDataString {
    
    NSString *returnValue = @"";
    if(self.postDataEncodingHandler)
        returnValue = self.postDataEncodingHandler(self.fieldsToBePosted);
    else if(self.postDataEncoding == MKNKPostDataEncodingTypeURL)
        returnValue = [self.fieldsToBePosted urlEncodedKeyValueString];
    else if(self.postDataEncoding == MKNKPostDataEncodingTypeJSON)
        returnValue = [self.fieldsToBePosted jsonEncodedKeyValueString];
    else if(self.postDataEncoding == MKNKPostDataEncodingTypePlist)
        returnValue = [self.fieldsToBePosted plistEncodedKeyValueString];
    return returnValue;
}

-(void) setCustomPostDataEncodingHandler:(MKNKEncodingBlock) postDataEncodingHandler forType:(NSString*) contentType {
    
    NSString *charset = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(self.stringEncoding));
    self.postDataEncoding = MKNKPostDataEncodingTypeCustom;
    self.postDataEncodingHandler = postDataEncodingHandler;
    [self.request setValue:
     [NSString stringWithFormat:@"%@; charset=%@", contentType, charset]
        forHTTPHeaderField:@"Content-Type"];
}
//===========================================================
//  freezable
//===========================================================
- (BOOL)freezable
{
    return _freezable;
}

-(NSString*) url {
    
    return [[self.request URL] absoluteString];
}

-(NSURLRequest*) readonlyRequest {
    
    return [self.request copy];
}

-(NSHTTPURLResponse*) readonlyResponse {
    
    return [self.response copy];
}

- (NSDictionary *) readonlyPostDictionary {
    
    return [self.fieldsToBePosted copy];
}

-(NSString*) HTTPMethod {
    
    return self.request.HTTPMethod;
}

-(NSInteger) HTTPStatusCode {
    
    if(self.response)
        return self.response.statusCode;
    else
        return 0;
}

- (void)setFreezable:(BOOL)flag
{
    // get method cannot be frozen.
    // No point in freezing a method that doesn't change server state.
    if([self.request.HTTPMethod isEqualToString:@"GET"] && flag) return;
    _freezable = flag;
    
    if(_freezable && self.uniqueId == nil)
        self.uniqueId = [NSString uniqueString];
}


-(BOOL) isEqual:(id)object {
    if(object == self)
        return YES;
    if(!object || ![object isKindOfClass:[self class]])
        return NO;
    if([self.request.HTTPMethod isEqualToString:@"GET"] || [self.request.HTTPMethod isEqualToString:@"HEAD"]) {
        
        MKNetworkOperation *anotherObject = (MKNetworkOperation*) object;
        return ([[self uniqueIdentifier] isEqualToString:[anotherObject uniqueIdentifier]]);
    }
    
    return NO;
}

-(NSUInteger) hash {
    return [[self uniqueIdentifier] hash];
}

-(NSString*) uniqueIdentifier {
    
    NSMutableString *str = [NSMutableString stringWithFormat:@"%@ %@", self.request.HTTPMethod, self.url];
    
    if(self.username || self.password) {
        
        [str appendFormat:@" [%@:%@]",
         self.username ? self.username : @"",
         self.password ? self.password : @""];
    }
    
    if(self.freezable) {
        
        [str appendString:self.uniqueId];
    }
    return [str md5];
}

-(BOOL) isCachedResponse {
    
    return self.cachedResponse != nil;
}

-(void) notifyCache {
    
    if(![self isCacheable]) return;
    if(!([self.response statusCode] >= 200 && [self.response statusCode] < 300)) return;
    
    if(![self isCancelled])
        self.cacheHandlingBlock(self);
}

-(MKNetworkOperationState) state {
    
    return (MKNetworkOperationState)_state;
}

-(void) setState:(MKNetworkOperationState)newState {
    
    switch (newState) {
        case MKNetworkOperationStateReady:
            [self willChangeValueForKey:@"isReady"];
            break;
        case MKNetworkOperationStateExecuting:
            [self willChangeValueForKey:@"isReady"];
            [self willChangeValueForKey:@"isExecuting"];
            break;
        case MKNetworkOperationStateFinished:
            [self willChangeValueForKey:@"isExecuting"];
            [self willChangeValueForKey:@"isFinished"];
            break;
    }
    
    _state = newState;
    
    switch (newState) {
        case MKNetworkOperationStateReady:
            [self didChangeValueForKey:@"isReady"];
            break;
        case MKNetworkOperationStateExecuting:
            [self didChangeValueForKey:@"isReady"];
            [self didChangeValueForKey:@"isExecuting"];
            break;
        case MKNetworkOperationStateFinished:
            [self didChangeValueForKey:@"isExecuting"];
            [self didChangeValueForKey:@"isFinished"];
            break;
    }
    
    if(self.operationStateChangedHandler) {
        self.operationStateChangedHandler(newState);
    }
}

- (void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeInteger:(NSInteger)self.stringEncoding forKey:@"stringEncoding"];
    [encoder encodeInteger:_postDataEncoding forKey:@"postDataEncoding"];
    
    [encoder encodeObject:self.uniqueId forKey:@"uniqueId"];
    [encoder encodeObject:self.request forKey:@"request"];
    [encoder encodeObject:self.response forKey:@"response"];
    [encoder encodeObject:self.fieldsToBePosted forKey:@"fieldsToBePosted"];
    [encoder encodeObject:self.filesToBePosted forKey:@"filesToBePosted"];
    [encoder encodeObject:self.dataToBePosted forKey:@"dataToBePosted"];
    [encoder encodeObject:self.username forKey:@"username"];
    [encoder encodeObject:self.password forKey:@"password"];
    [encoder encodeObject:self.clientCertificate forKey:@"clientCertificate"];
    [encoder encodeObject:self.clientCertificatePassword forKey:@"clientCertificatePassword"];
    [encoder encodeBool:self.shouldContinueWithInvalidCertificate forKey:@"shouldContinueWithInvalidCertificate"];
#if TARGET_OS_IPHONE
    [encoder encodeObject:self.localNotification forKey:@"localNotification"];
#endif
    self.state = MKNetworkOperationStateReady;
    [encoder encodeInt32:_state forKey:@"state"];
    [encoder encodeBool:self.isCancelled forKey:@"isCancelled"];
    [encoder encodeObject:self.mutableData forKey:@"mutableData"];
    [encoder encodeInteger:(NSInteger)self.downloadedDataSize forKey:@"downloadedDataSize"];
    [encoder encodeObject:self.downloadStreams forKey:@"downloadStreams"];
    [encoder encodeInteger:self.startPosition forKey:@"startPosition"];
    [encoder encodeInteger:self.credentialPersistence forKey:@"credentialPersistence"];
}

- (instancetype)initWithCoder:(NSCoder *)decoder
{
    self = [super init];
    if (self) {
        [self setStringEncoding:(NSStringEncoding)[decoder decodeIntegerForKey:@"stringEncoding"]];
        _postDataEncoding = (MKNKPostDataEncodingType) [decoder decodeIntegerForKey:@"postDataEncoding"];
        self.request = [decoder decodeObjectForKey:@"request"];
        self.uniqueId = [decoder decodeObjectForKey:@"uniqueId"];
        
        self.response = [decoder decodeObjectForKey:@"response"];
        self.fieldsToBePosted = [decoder decodeObjectForKey:@"fieldsToBePosted"];
        self.filesToBePosted = [decoder decodeObjectForKey:@"filesToBePosted"];
        self.dataToBePosted = [decoder decodeObjectForKey:@"dataToBePosted"];
        self.username = [decoder decodeObjectForKey:@"username"];
        self.password = [decoder decodeObjectForKey:@"password"];
        self.clientCertificate = [decoder decodeObjectForKey:@"clientCertificate"];
        self.clientCertificatePassword = [decoder decodeObjectForKey:@"clientCertificatePassword"];
#if TARGET_OS_IPHONE
        self.localNotification = [decoder decodeObjectForKey:@"localNotification"];
#endif
        [self setState:(MKNetworkOperationState)[decoder decodeInt32ForKey:@"state"]];
        self.isCancelled = [decoder decodeBoolForKey:@"isCancelled"];
        self.mutableData = [decoder decodeObjectForKey:@"mutableData"];
        self.downloadedDataSize = [decoder decodeIntegerForKey:@"downloadedDataSize"];
        self.downloadStreams = [decoder decodeObjectForKey:@"downloadStreams"];
        self.startPosition = [decoder decodeIntegerForKey:@"startPosition"];
        self.credentialPersistence = [decoder decodeIntegerForKey:@"credentialPersistence"];
    }
    return self;
}

- (instancetype)copyWithZone:(NSZone *)zone
{
    MKNetworkOperation *theCopy = [[[self class] allocWithZone:zone] init];  // use designated initializer
    
    theCopy.postDataEncoding = _postDataEncoding;
    [theCopy setStringEncoding:self.stringEncoding];
    [theCopy setUniqueId:[self.uniqueId copy]];
    
    [theCopy setConnection:[self.connection copy]];
    [theCopy setRequest:[self.request copy]];
    [theCopy setResponse:[self.response copy]];
    [theCopy setFieldsToBePosted:[self.fieldsToBePosted copy]];
    [theCopy setFilesToBePosted:[self.filesToBePosted copy]];
    [theCopy setDataToBePosted:[self.dataToBePosted copy]];
    [theCopy setUsername:[self.username copy]];
    [theCopy setPassword:[self.password copy]];
    [theCopy setClientCertificate:[self.clientCertificate copy]];
    [theCopy setClientCertificatePassword:[self.clientCertificatePassword copy]];
    [theCopy setResponseBlocks:[self.responseBlocks copy]];
    [theCopy setErrorBlocks:[self.errorBlocks copy]];
    [theCopy setErrorBlocksType2:[self.errorBlocksType2 copy]];
    [theCopy setState:self.state];
    [theCopy setIsCancelled:self.isCancelled];
    [theCopy setMutableData:[self.mutableData copy]];
    [theCopy setDownloadedDataSize:self.downloadedDataSize];
    [theCopy setNotModifiedHandlers:[self.notModifiedHandlers copy]];
    [theCopy setUploadProgressChangedHandlers:[self.uploadProgressChangedHandlers copy]];
    [theCopy setDownloadProgressChangedHandlers:[self.downloadProgressChangedHandlers copy]];
    [theCopy setSegmentedDownloadChangedHandlers:[self.segmentedDownloadChangedHandlers copy]];
    [theCopy setDownloadStreams:[self.downloadStreams copy]];
    [theCopy setCachedResponse:[self.cachedResponse copy]];
    [theCopy setCacheHandlingBlock:self.cacheHandlingBlock];
    [theCopy setStartPosition:self.startPosition];
    [theCopy setCredentialPersistence:self.credentialPersistence];
    
    return theCopy;
}


- (instancetype)mutableCopyWithZone:(NSZone *)zone
{
    MKNetworkOperation *theCopy = [[[self class] allocWithZone:zone] init];  // use designated initializer
    
    theCopy.postDataEncoding = _postDataEncoding;
    [theCopy setStringEncoding:self.stringEncoding];
    [theCopy setUniqueId:[self.uniqueId copy]];
    
    [theCopy setConnection:[self.connection mutableCopy]];
    [theCopy setRequest:[self.request mutableCopy]];
    [theCopy setResponse:[self.response mutableCopy]];
    [theCopy setFieldsToBePosted:[self.fieldsToBePosted mutableCopy]];
    [theCopy setFilesToBePosted:[self.filesToBePosted mutableCopy]];
    [theCopy setDataToBePosted:[self.dataToBePosted mutableCopy]];
    [theCopy setUsername:[self.username copy]];
    [theCopy setPassword:[self.password copy]];
    [theCopy setClientCertificate:[self.clientCertificate copy]];
    [theCopy setClientCertificatePassword:[self.clientCertificatePassword copy]];
    [theCopy setResponseBlocks:[self.responseBlocks mutableCopy]];
    [theCopy setErrorBlocks:[self.errorBlocks mutableCopy]];
    [theCopy setErrorBlocksType2:[self.errorBlocksType2 mutableCopy]];
    [theCopy setState:self.state];
    [theCopy setIsCancelled:self.isCancelled];
    [theCopy setMutableData:[self.mutableData mutableCopy]];
    [theCopy setDownloadedDataSize:self.downloadedDataSize];
    [theCopy setNotModifiedHandlers:[self.notModifiedHandlers mutableCopy]];
    [theCopy setUploadProgressChangedHandlers:[self.uploadProgressChangedHandlers mutableCopy]];
    [theCopy setDownloadProgressChangedHandlers:[self.downloadProgressChangedHandlers mutableCopy]];
    [theCopy setSegmentedDownloadChangedHandlers:[self.segmentedDownloadChangedHandlers mutableCopy]];
    [theCopy setDownloadStreams:[self.downloadStreams mutableCopy]];
    [theCopy setCachedResponse:[self.cachedResponse mutableCopy]];
    [theCopy setCacheHandlingBlock:self.cacheHandlingBlock];
    [theCopy setStartPosition:self.startPosition];
    [theCopy setCredentialPersistence:self.credentialPersistence];
    
    return theCopy;
}

- (instancetype)copyForRetry
{
    MKNetworkOperation *theCopy = [[[self class] alloc] init];
    
    [theCopy setConnection:nil];
    [theCopy setResponse:nil];
    [theCopy setState:MKNetworkOperationStateReady];
    [theCopy setIsCancelled:NO];
    [theCopy setDownloadedDataSize:0];
    [theCopy setStartPosition:0];
    
    theCopy.postDataEncoding = _postDataEncoding;
    [theCopy setStringEncoding:self.stringEncoding];
    [theCopy setUniqueId:[self.uniqueId copy]];
    [theCopy setRequest:[self.request copy]];
    [theCopy setFieldsToBePosted:[self.fieldsToBePosted mutableCopy]];
    [theCopy setFilesToBePosted:[self.filesToBePosted mutableCopy]];
    [theCopy setDataToBePosted:[self.dataToBePosted mutableCopy]];
    [theCopy setUsername:[self.username copy]];
    [theCopy setPassword:[self.password copy]];
    [theCopy setClientCertificate:[self.clientCertificate copy]];
    [theCopy setClientCertificatePassword:[self.clientCertificatePassword copy]];
    [theCopy setResponseBlocks:[self.responseBlocks mutableCopy]];
    [theCopy setErrorBlocks:[self.errorBlocks mutableCopy]];
    [theCopy setErrorBlocksType2:[self.errorBlocksType2 mutableCopy]];
    [theCopy setMutableData:[self.mutableData mutableCopy]];
    [theCopy setNotModifiedHandlers:[self.notModifiedHandlers mutableCopy]];
    [theCopy setUploadProgressChangedHandlers:[self.uploadProgressChangedHandlers mutableCopy]];
    [theCopy setDownloadProgressChangedHandlers:[self.downloadProgressChangedHandlers mutableCopy]];
    [theCopy setSegmentedDownloadChangedHandlers:[self.segmentedDownloadChangedHandlers mutableCopy]];
    [theCopy setDownloadStreams:[self.downloadStreams mutableCopy]];
    [theCopy setCachedResponse:[self.cachedResponse mutableCopy]];
    [theCopy setCacheHandlingBlock:self.cacheHandlingBlock];
    [theCopy setCredentialPersistence:self.credentialPersistence];
    
    return theCopy;
}

-(void) dealloc {
    
    [_connection cancel];
    _connection = nil;
}

-(void) updateHandlersFromOperation:(MKNetworkOperation*) operation {
    
    [self.responseBlocks addObjectsFromArray:operation.responseBlocks];
    [self.errorBlocks addObjectsFromArray:operation.errorBlocks];
    [self.errorBlocksType2 addObjectsFromArray:operation.errorBlocksType2];
    [self.notModifiedHandlers addObjectsFromArray:operation.notModifiedHandlers];
    [self.uploadProgressChangedHandlers addObjectsFromArray:operation.uploadProgressChangedHandlers];
    [self.downloadProgressChangedHandlers addObjectsFromArray:operation.downloadProgressChangedHandlers];
    [self.segmentedDownloadChangedHandlers addObjectsFromArray:operation.segmentedDownloadChangedHandlers];
    [self.downloadStreams addObjectsFromArray:operation.downloadStreams];
}

-(void) setCachedData:(NSData*) cachedData {
    
    self.cachedResponse = cachedData;
    [self operationSucceeded];
}

-(void) updateOperationBasedOnPreviousHeaders:(NSMutableDictionary*) headers {
    
    NSString *lastModified = headers[@"Last-Modified"];
    NSString *eTag = headers[@"ETag"];
    if(lastModified) {
        [self.request setValue:lastModified forHTTPHeaderField:@"IF-MODIFIED-SINCE"];
    }
    
    if(eTag) {
        [self.request setValue:eTag forHTTPHeaderField:@"IF-NONE-MATCH"];
    }
}

-(void) setUsername:(NSString*) username password:(NSString*) password {
    
    self.username = username;
    self.password = password;
}

-(void) setUsername:(NSString*) username password:(NSString*) password basicAuth:(BOOL) bYesOrNo {
    
    [self setUsername:username password:password];
    NSString *base64EncodedString = [[[NSString stringWithFormat:@"%@:%@", self.username, self.password] dataUsingEncoding:NSUTF8StringEncoding] base64EncodedString];
    
    [self setAuthorizationHeaderValue:base64EncodedString forAuthType:@"Basic"];
}

-(void) onCompletion:(MKNKResponseBlock) response onError:(MKNKErrorBlock) error {
    
    [self.responseBlocks addObject:[response copy]];
    [self.errorBlocks addObject:[error copy]];
}

-(void) addCompletionHandler:(MKNKResponseBlock)response errorHandler:(MKNKResponseErrorBlock)error {
    
    if(response)
        [self.responseBlocks addObject:[response copy]];
    if(error)
        [self.errorBlocksType2 addObject:[error copy]];
}

-(void) onNotModified:(MKNKVoidBlock)notModifiedBlock {
    
    [self.notModifiedHandlers addObject:[notModifiedBlock copy]];
}

-(void) onUploadProgressChanged:(MKNKProgressBlock) uploadProgressBlock {
    
    [self.uploadProgressChangedHandlers addObject:[uploadProgressBlock copy]];
}

-(void) onDownloadProgressChanged:(MKNKProgressBlock) downloadProgressBlock {
    
    [self.downloadProgressChangedHandlers addObject:[downloadProgressBlock copy]];
}

- (void)onSegmentedDownloadChanged:(MKNKSegmentedDownloadBlock)segmentedDownloadBlock
{
    [self.segmentedDownloadChangedHandlers addObject:[segmentedDownloadBlock copy]];
}

-(void) setUploadStream:(NSInputStream*) inputStream {
    
    self.request.HTTPBodyStream = inputStream;
}

-(void) addDownloadStream:(NSOutputStream*) outputStream {
    
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [self.downloadStreams addObject:outputStream];
}

- (id)initWithURLString:(NSString *)aURLString
                 params:(NSDictionary *)params
             httpMethod:(NSString *)method

{
    if((self = [super init])) {
        
        self.responseBlocks = [NSMutableArray array];
        self.errorBlocks = [NSMutableArray array];
        self.errorBlocksType2 = [NSMutableArray array];
        self.filesToBePosted = [NSMutableArray array];
        self.dataToBePosted = [NSMutableArray array];
        self.fieldsToBePosted = [NSMutableDictionary dictionary];
        
        self.notModifiedHandlers = [NSMutableArray array];
        self.uploadProgressChangedHandlers = [NSMutableArray array];
        self.downloadProgressChangedHandlers = [NSMutableArray array];
        self.segmentedDownloadChangedHandlers = [NSMutableArray array];
        self.downloadStreams = [NSMutableArray array];
        
        self.credentialPersistence = NSURLCredentialPersistenceForSession;
        
        NSURL *finalURL = nil;
        
        if(params)
            self.fieldsToBePosted = [params mutableCopy];
        
        self.stringEncoding = NSUTF8StringEncoding; // use a delegate to get these values later
        
        if(!method) method = @"GET";
        
        if ([method isEqualToString:@"GET"])
            self.cacheHeaders = [NSMutableDictionary dictionary];
        
        if (([method isEqualToString:@"GET"] ||
             [method isEqualToString:@"DELETE"]) && (params && [params count] > 0)) {
            
            finalURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@?%@", aURLString,
                                             [self.fieldsToBePosted urlEncodedKeyValueString]]];
        } else {
            finalURL = [NSURL URLWithString:aURLString];
        }
        
        if(finalURL == nil) {
            
            DLog(@"Cannot create a URL with %@ and parameters %@ and method %@", aURLString, self.fieldsToBePosted, method);
            return nil;
        }
        
        self.request = [NSMutableURLRequest requestWithURL:finalURL
                                               cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                           timeoutInterval:kMKNetworkKitRequestTimeOutInSeconds];
        
        [self.request setHTTPMethod:method];
        
        if (([method isEqualToString:@"POST"] ||
             [method isEqualToString:@"PUT"]) && (params && [params count] > 0)) {
            
            self.postDataEncoding = MKNKPostDataEncodingTypeURL;
        }
        
        self.state = MKNetworkOperationStateReady;
    }
    
    return self;
}

-(void) addParams:(NSDictionary*) paramsDictionary {
    
    [self.fieldsToBePosted addEntriesFromDictionary:paramsDictionary];
}

-(void) addHeaders:(NSDictionary*) headersDictionary {
    
    [headersDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        [self.request addValue:obj forHTTPHeaderField:key];
    }];
}

-(void) addHeader:(NSString*)key withValue:(NSString*)value {
    
    [self.request addValue:value forHTTPHeaderField:key];
}

-(void) setHeader:(NSString*)key withValue:(NSString*)value {
    
    [self.request setValue:value forHTTPHeaderField:key];
}

-(void) setAuthorizationHeaderValue:(NSString*) token forAuthType:(NSString*) authType {
    
    [self.request setValue:[NSString stringWithFormat:@"%@ %@", authType, token]
        forHTTPHeaderField:@"Authorization"];
}
/*
 Printing a MKNetworkOperation object is printed in curl syntax
 */

-(NSString*) description {
    
    NSMutableString *displayString = [NSMutableString stringWithFormat:@"%@\nRequest\n-------\n%@",
                                      [[NSDate date] descriptionWithLocale:[NSLocale currentLocale]],
                                      [self curlCommandLineString]];
    
    NSString *responseString = [self responseString];
    if([responseString length] > 0) {
        [displayString appendFormat:@"\n--------\nResponse\n--------\n%@\n", responseString];
    }
    
    return displayString;
}

-(NSString*) curlCommandLineString
{
    __block NSMutableString *displayString = [NSMutableString stringWithFormat:@"curl -X %@", self.request.HTTPMethod];
    
    if([self.filesToBePosted count] == 0 && [self.dataToBePosted count] == 0) {
        [[self.request allHTTPHeaderFields] enumerateKeysAndObjectsUsingBlock:^(id key, id val, BOOL *stop)
         {
             [displayString appendFormat:@" -H \'%@: %@\'", key, val];
         }];
    }
    
    [displayString appendFormat:@" \'%@\'",  self.url];
    
    if ([self.request.HTTPMethod isEqualToString:@"POST"] ||
        [self.request.HTTPMethod isEqualToString:@"PUT"] ||
        [self.request.HTTPMethod isEqualToString:@"PATCH"]) {
        
        NSString *option = [self.filesToBePosted count] == 0 ? @"-d" : @"-F";
        if(self.postDataEncoding == MKNKPostDataEncodingTypeURL) {
            [self.fieldsToBePosted enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
                
                [displayString appendFormat:@" %@ \'%@=%@\'", option, key, obj];
            }];
        } else {
            [displayString appendFormat:@" -d \'%@\'", [self encodedPostDataString]];
        }
        
        
        [self.filesToBePosted enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            
            NSDictionary *thisFile = (NSDictionary*) obj;
            [displayString appendFormat:@" -F \'%@=@%@;type=%@\'", thisFile[@"name"],
             thisFile[@"filepath"], thisFile[@"mimetype"]];
        }];
        
        /* Not sure how to do this via curl
         [self.dataToBePosted enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
         
         NSDictionary *thisData = (NSDictionary*) obj;
         [displayString appendFormat:@" --data-binary \"%@\"", [thisData objectForKey:@"data"]];
         }];*/
    }
    
    return displayString;
}


-(void) addData:(NSData*) data forKey:(NSString*) key {
    
    [self addData:data forKey:key mimeType:@"application/octet-stream" fileName:@"file"];
}

-(void) addData:(NSData*) data forKey:(NSString*) key mimeType:(NSString*) mimeType fileName:(NSString*) fileName {
    
    if ([self.request.HTTPMethod isEqualToString:@"GET"]) {
        [self.request setHTTPMethod:@"POST"];
    }
    
    NSDictionary *dict = @{@"data": data,
                           @"name": key,
                           @"mimetype": mimeType,
                           @"filename": fileName};
    
    [self.dataToBePosted addObject:dict];
}

-(void) addFile:(NSString*) filePath forKey:(NSString*) key {
    
    [self addFile:filePath forKey:key mimeType:@"application/octet-stream"];
}

-(void) addFile:(NSString*) filePath forKey:(NSString*) key mimeType:(NSString*) mimeType {
    
    if ([self.request.HTTPMethod isEqualToString:@"GET"]) {
        [self.request setHTTPMethod:@"POST"];
    }
    
    NSDictionary *dict = @{@"filepath": filePath,
                           @"name": key,
                           @"mimetype": mimeType};
    
    [self.filesToBePosted addObject:dict];
}

-(NSData*) bodyData {
    
    if([self.filesToBePosted count] == 0 && [self.dataToBePosted count] == 0) {
        
        return [[self encodedPostDataString] dataUsingEncoding:self.stringEncoding];
    }
    
    NSString *boundary = @"0xKhTmLbOuNdArY";
    NSMutableData *body = [NSMutableData data];
    __block NSUInteger postLength = 0;
    
    [self.fieldsToBePosted enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        
        NSString *thisFieldString = [NSString stringWithFormat:
                                     @"--%@\r\nContent-Disposition: form-data; name=\"%@\"\r\n\r\n%@",
                                     boundary, key, obj];
        
        [body appendData:[thisFieldString dataUsingEncoding:[self stringEncoding]]];
        [body appendData:[@"\r\n" dataUsingEncoding:[self stringEncoding]]];
    }];
    
    [self.filesToBePosted enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        
        NSDictionary *thisFile = (NSDictionary*) obj;
        NSString *thisFieldString = [NSString stringWithFormat:
                                     @"--%@\r\nContent-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\nContent-Type: %@\r\nContent-Transfer-Encoding: binary\r\n\r\n",
                                     boundary,
                                     thisFile[@"name"],
                                     [thisFile[@"filepath"] lastPathComponent],
                                     thisFile[@"mimetype"]];
        
        [body appendData:[thisFieldString dataUsingEncoding:[self stringEncoding]]];
        [body appendData: [NSData dataWithContentsOfFile:thisFile[@"filepath"]]];
        [body appendData:[@"\r\n" dataUsingEncoding:[self stringEncoding]]];
    }];
    
    [self.dataToBePosted enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        
        NSDictionary *thisDataObject = (NSDictionary*) obj;
        NSString *thisFieldString = [NSString stringWithFormat:
                                     @"--%@\r\nContent-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\nContent-Type: %@\r\nContent-Transfer-Encoding: binary\r\n\r\n",
                                     boundary,
                                     thisDataObject[@"name"],
                                     thisDataObject[@"filename"],
                                     thisDataObject[@"mimetype"]];
        
        [body appendData:[thisFieldString dataUsingEncoding:[self stringEncoding]]];
        [body appendData:thisDataObject[@"data"]];
        [body appendData:[@"\r\n" dataUsingEncoding:[self stringEncoding]]];
    }];
    
    if (postLength >= 1)
        [self.request setValue:[NSString stringWithFormat:@"%lu", (unsigned long) postLength] forHTTPHeaderField:@"Content-Length"];
    
    [body appendData: [[NSString stringWithFormat:@"--%@--\r\n", boundary] dataUsingEncoding:self.stringEncoding]];
    
    NSString *charset = (__bridge NSString *)CFStringConvertEncodingToIANACharSetName(CFStringConvertNSStringEncodingToEncoding(self.stringEncoding));
    
    if(([self.filesToBePosted count] > 0) || ([self.dataToBePosted count] > 0)) {
        [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; charset=%@; boundary=%@", charset, boundary]
            forHTTPHeaderField:@"Content-Type"];
        
        [self.request setValue:[NSString stringWithFormat:@"%lu", (unsigned long) [body length]] forHTTPHeaderField:@"Content-Length"];
    }
    
    return body;
}


-(void) setCacheHandler:(MKNKResponseBlock) cacheHandler {
    
    self.cacheHandlingBlock = cacheHandler;
}

#pragma mark -
#pragma Main method
-(void) main {
    
    @autoreleasepool {
        [self start];
    }
}

-(void) endBackgroundTask {
    
#if TARGET_OS_IPHONE
    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
            [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
            self.backgroundTaskId = UIBackgroundTaskInvalid;
        }
    });
#endif
}

- (void) start
{
    
#if TARGET_OS_IPHONE
    self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        
        dispatch_async(dispatch_get_main_queue(), ^{
            if (self.backgroundTaskId != UIBackgroundTaskInvalid)
            {
                [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
                self.backgroundTaskId = UIBackgroundTaskInvalid;
                [self cancel];
            }
        });
    }];
    
#endif
    
    if(!self.isCancelled) {
        
        if (([self.request.HTTPMethod isEqualToString:@"POST"] ||
             [self.request.HTTPMethod isEqualToString:@"PUT"] ||
             [self.request.HTTPMethod isEqualToString:@"PATCH"]) && !self.request.HTTPBodyStream) {
            
            [self.request setHTTPBody:[self bodyData]];
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            self.connection = [[NSURLConnection alloc] initWithRequest:self.request
                                                              delegate:self
                                                      startImmediately:NO];
            
            [self.connection scheduleInRunLoop:[NSRunLoop currentRunLoop]
                                       forMode:NSRunLoopCommonModes];
            
            [self.connection start];
        });
        
        self.state = MKNetworkOperationStateExecuting;
    }
    else {
        self.state = MKNetworkOperationStateFinished;
        [self endBackgroundTask];
    }
}

#pragma -
#pragma mark NSOperation stuff

- (BOOL)isConcurrent
{
    return YES;
}

- (BOOL)isReady {
    
    return (self.state == MKNetworkOperationStateReady && [super isReady]);
}

- (BOOL)isFinished
{
    return (self.state == MKNetworkOperationStateFinished);
}

- (BOOL)isExecuting {
    
    return (self.state == MKNetworkOperationStateExecuting);
}

-(void) cancel {
    
    if([self isFinished])
        return;
    
    @synchronized(self) {
        self.isCancelled = YES;
        
        [self.connection cancel];
        
        [self.responseBlocks removeAllObjects];
        self.responseBlocks = nil;
        
        [self.errorBlocks removeAllObjects];
        self.errorBlocks = nil;
        
        [self.errorBlocksType2 removeAllObjects];
        self.errorBlocksType2 = nil;
        
        [self.notModifiedHandlers removeAllObjects];
        self.notModifiedHandlers = nil;
        
        [self.uploadProgressChangedHandlers removeAllObjects];
        self.uploadProgressChangedHandlers = nil;
        
        [self.downloadProgressChangedHandlers removeAllObjects];
        self.downloadProgressChangedHandlers = nil;
        
        [self.segmentedDownloadChangedHandlers removeAllObjects];
        self.segmentedDownloadChangedHandlers = nil;
        
        for(NSOutputStream *stream in self.downloadStreams)
        {
            [stream close];
        }
        
        [self.downloadStreams removeAllObjects];
        self.downloadStreams = nil;
        
        self.authHandler = nil;
        self.mutableData = nil;
        self.downloadedDataSize = 0;
        
        self.cacheHandlingBlock = nil;
        
        if(self.state == MKNetworkOperationStateExecuting)
            self.state = MKNetworkOperationStateFinished; // This notifies the queue and removes the operation.
        // if the operation is not removed, the spinner continues to spin, not a good UX
        
        [self endBackgroundTask];
    }
    [super cancel];
}

#pragma mark -
#pragma mark NSURLConnection delegates

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    
    self.state = MKNetworkOperationStateFinished;
    self.mutableData = nil;
    self.downloadedDataSize = 0;
    for(NSOutputStream *stream in self.downloadStreams)
        [stream close];
    
    [self operationFailedWithError:error];
    [self endBackgroundTask];
}

// https://developer.apple.com/library/mac/#documentation/security/conceptual/CertKeyTrustProgGuide/iPhone_Tasks/iPhone_Tasks.html
OSStatus extractIdentityAndTrust(CFDataRef inPKCS12Data,        // 5
                                 SecIdentityRef *outIdentity,
                                 SecTrustRef *outTrust,
                                 CFStringRef keyPassword)
{
    OSStatus securityError = errSecSuccess;
    
    
    const void *keys[] =   { kSecImportExportPassphrase };
    const void *values[] = { keyPassword };
    CFDictionaryRef optionsDictionary = NULL;
    
    /* Create a dictionary containing the passphrase if one
     was specified.  Otherwise, create an empty dictionary. */
    optionsDictionary = CFDictionaryCreate(
                                           NULL, keys,
                                           values, (keyPassword ? 1 : 0),
                                           NULL, NULL);  // 6
    
    CFArrayRef items = NULL;
    securityError = SecPKCS12Import(inPKCS12Data,
                                    optionsDictionary,
                                    &items);                    // 7
    
    
    //
    if (securityError == 0) {                                   // 8
        CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
        const void *tempIdentity = NULL;
        tempIdentity = CFDictionaryGetValue (myIdentityAndTrust,
                                             kSecImportItemIdentity);
        CFRetain(tempIdentity);
        *outIdentity = (SecIdentityRef)tempIdentity;
        const void *tempTrust = NULL;
        tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
        
        CFRetain(tempTrust);
        *outTrust = (SecTrustRef)tempTrust;
    }
    
    if (optionsDictionary)
        CFRelease(optionsDictionary);                           // 9
    
    if (items)
        CFRelease(items);
    
    return securityError;
}

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    
    if (challenge.previousFailureCount == 0) {
        
        if (((challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault) ||
             (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic) ||
             (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest) ||
             (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM)) &&
            (self.username && self.password))
        {
            
            // for NTLM, we will assume user name to be of the form "domain\\username"
            NSURLCredential *credential = [NSURLCredential credentialWithUser:self.username
                                                                     password:self.password
                                                                  persistence:self.credentialPersistence];
            
            [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
        }
        else if ((challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate) && self.clientCertificate) {
            
            NSError *error = nil;
            NSData *certData = [[NSData alloc] initWithContentsOfFile:self.clientCertificate options:0 error:&error];
            
            SecIdentityRef identity;
            SecTrustRef trust;
            OSStatus status = extractIdentityAndTrust((__bridge CFDataRef) certData, &identity, &trust, (__bridge CFStringRef) self.clientCertificatePassword);
            if(status == errSecSuccess) {
                SecCertificateRef certificate;
                SecIdentityCopyCertificate(identity, &certificate);
                const void *certs[] = { certificate };
                CFArrayRef certsArray = CFArrayCreate(NULL, certs, 1, NULL);
                NSArray *certificatesForCredential = (__bridge NSArray *)certsArray;
                NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity
                                                                         certificates:certificatesForCredential
                                                                          persistence:NSURLCredentialPersistencePermanent];
                [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
                CFRelease(identity);
                CFRelease(certificate);
                CFRelease(certsArray);
            } else {
                [challenge.sender cancelAuthenticationChallenge:challenge];
            }
        }
        else if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
            
            if(challenge.previousFailureCount < 5) {
                
                self.serverTrust = challenge.protectionSpace.serverTrust;
                SecTrustResultType result;
                SecTrustEvaluate(self.serverTrust, &result);
                
                if(result == kSecTrustResultProceed ||
                   result == kSecTrustResultUnspecified //The cert is valid, but user has not explicitly accepted/denied. Ok to proceed (Ch 15: iOS PTL :Pg 269)
                   ) {
                    
                    [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
                } else {
                    
                    // invalid or revoked certificate
                    if(self.shouldContinueWithInvalidCertificate) {
                        DLog(@"Certificate is invalid, but self.shouldContinueWithInvalidCertificate is YES");
                        [challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
                    } else {
                        DLog(@"Certificate is invalid, continuing without credentials. Might result in 401 Unauthorized");
                        [challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
                    }
                }
            } else {
                
                [challenge.sender cancelAuthenticationChallenge:challenge];
            }
        }
        else if (self.authHandler) {
            
            // forward the authentication to the view controller that created this operation
            // If this happens for NSURLAuthenticationMethodHTMLForm, you have to
            // do some shit work like showing a modal webview controller and close it after authentication.
            // I HATE THIS.
            self.authHandler(challenge);
        }
        else {
            [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
        }
    } else {
        //  apple proposes to cancel authentication, which results in NSURLErrorDomain error -1012, but we prefer to trigger a 401
        //        [[challenge sender] cancelAuthenticationChallenge:challenge];
        [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    
    NSUInteger size = [self.response expectedContentLength] < 0 ? 0 : (NSUInteger)[self.response expectedContentLength];
    self.response = (NSHTTPURLResponse*) response;
    
    // dont' save data if the operation was created to download directly to a stream.
    if([self.downloadStreams count] == 0)
        self.mutableData = [NSMutableData dataWithCapacity:size];
    else
        self.mutableData = nil;
    
    for(NSOutputStream *stream in self.downloadStreams)
        [stream open];
    
    NSDictionary *httpHeaders = [self.response allHeaderFields];
    
    // if you attach a stream to the operation, MKNetworkKit will not cache the response.
    // Streams are usually "big data chunks" that doesn't need caching anyways.
    
    if([self.request.HTTPMethod isEqualToString:@"GET"] && [self.downloadStreams count] == 0) {
        
        // We have all this complicated cache handling since NSURLRequestReloadRevalidatingCacheData is not implemented
        // do cache processing only if the request is a "GET" method
        NSString *lastModified = httpHeaders[@"Last-Modified"];
        NSString *eTag = httpHeaders[@"ETag"];
        NSString *expiresOn = httpHeaders[@"Expires"];
        
        NSString *contentType = httpHeaders[@"Content-Type"];
        // if contentType is image,
        
        NSDate *expiresOnDate = nil;
        
        if([contentType rangeOfString:@"image"].location != NSNotFound) {
            
            // For images let's assume a expiry date of 7 days if there is no eTag or Last Modified.
            if(!eTag && !lastModified)
                expiresOnDate = [[NSDate date] dateByAddingTimeInterval:kMKNetworkKitDefaultImageCacheDuration];
            else
                expiresOnDate = [[NSDate date] dateByAddingTimeInterval:kMKNetworkKitDefaultImageHeadRequestDuration];
        }
        
        NSString *cacheControl = httpHeaders[@"Cache-Control"]; // max-age, must-revalidate, no-cache
        NSArray *cacheControlEntities = [cacheControl componentsSeparatedByString:@","];
        
        for(NSString *substring in cacheControlEntities) {
            
            if([substring rangeOfString:@"max-age"].location != NSNotFound) {
                
                // do some processing to calculate expiresOn
                NSString *maxAge = nil;
                NSArray *array = [substring componentsSeparatedByString:@"="];
                if([array count] > 1)
                    maxAge = array[1];
                
                expiresOnDate = [[NSDate date] dateByAddingTimeInterval:[maxAge intValue]];
            }
            if([substring rangeOfString:@"no-cache"].location != NSNotFound) {
                
                // Don't cache this request
                expiresOnDate = [[NSDate date] dateByAddingTimeInterval:kMKNetworkKitDefaultCacheDuration];
            }
        }
        
        // if there was a cacheControl entity, we would have a expiresOnDate that is not nil.
        // "Cache-Control" headers take precedence over "Expires" headers
        
        if(expiresOnDate)
            expiresOn = [expiresOnDate rfc1123String];
        
        // now remember lastModified, eTag and expires for this request in cache
        if(expiresOn)
            (self.cacheHeaders)[@"Expires"] = expiresOn;
        if(lastModified)
            (self.cacheHeaders)[@"Last-Modified"] = lastModified;
        if(eTag)
            (self.cacheHeaders)[@"ETag"] = eTag;
    }
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    
    if (self.downloadedDataSize == 0) {
        // This is the first batch of data
        // Check for a range header and make changes as neccesary
        NSString *rangeString = [[self request] valueForHTTPHeaderField:@"Range"];
        if ([rangeString hasPrefix:@"bytes="] && [rangeString hasSuffix:@"-"]) {
            NSString *bytesText = [rangeString substringWithRange:NSMakeRange(6, [rangeString length] - 7)];
            self.startPosition = [bytesText integerValue];
            self.downloadedDataSize = self.startPosition;
            DLog(@"Resuming at %lu bytes", (unsigned long) self.startPosition);
        }
    }
    
    if([self.downloadStreams count] == 0)
    {
        [self.mutableData appendData:data];
    }
    
    for(NSOutputStream *stream in self.downloadStreams)
    {
        if ([stream hasSpaceAvailable])
        {
            const uint8_t *dataBuffer = [data bytes];
            [stream write:&dataBuffer[0] maxLength:[data length]];
        }
    }
    
    self.downloadedDataSize += [data length];
    
    if (data.length)
    {
        for (MKNKSegmentedDownloadBlock block in self.segmentedDownloadChangedHandlers) {
            block(data);
        }
    }
    
    for(MKNKProgressBlock downloadProgressBlock in self.downloadProgressChangedHandlers) {
        
        if([self.response expectedContentLength] > 0) {
            
            double progress = (double)(self.downloadedDataSize) / (double)(self.startPosition + [self.response expectedContentLength]);
            downloadProgressBlock(progress);
        }
    }
}

- (void)connection:(NSURLConnection *)connection didSendBodyData:(NSInteger)bytesWritten
 totalBytesWritten:(NSInteger)totalBytesWritten
totalBytesExpectedToWrite:(NSInteger)totalBytesExpectedToWrite {
    
    for(MKNKProgressBlock uploadProgressBlock in self.uploadProgressChangedHandlers) {
        
        if(totalBytesExpectedToWrite > 0) {
            uploadProgressBlock(((double)totalBytesWritten/(double)totalBytesExpectedToWrite));
        }
    }
}

// http://stackoverflow.com/questions/1446509/handling-redirects-correctly-with-nsurlconnection
- (NSURLRequest *)connection: (NSURLConnection *)inConnection
             willSendRequest: (NSURLRequest *)inRequest
            redirectResponse: (NSURLResponse *)inRedirectResponse;
{
    NSMutableURLRequest *r = [self.request mutableCopy];
    if (inRedirectResponse) {
        [r setURL: [inRequest URL]];
    } else {
        // Note that we need to configure the Accept-Language header this late in processing
        // because NSURLRequest adds a default Accept-Language header late in the day, so we
        // have to undo that here.
        // For discussion see:
        // http://lists.apple.com/archives/macnetworkprog/2009/Sep/msg00022.html
        // http://stackoverflow.com/questions/5695914/nsurlrequest-where-an-app-can-find-the-default-headers-for-http-request
        NSString* accept_language = self.shouldSendAcceptLanguageHeader ? [self languagesFromLocale] : nil;
        [r setValue:accept_language forHTTPHeaderField:@"Accept-Language"];
    }
    return r;
}

- (NSString*)languagesFromLocale {
    return [NSString stringWithFormat:@"%@, en-us", [[NSLocale preferredLanguages] componentsJoinedByString:@", "]];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    
    if([self isCancelled])
        return;
    
    self.state = MKNetworkOperationStateFinished;
    
    for(NSOutputStream *stream in self.downloadStreams)
        [stream close];
    
    if (self.response.statusCode >= 200 && self.response.statusCode < 300 && ![self isCancelled]) {
        
        self.cachedResponse = nil; // remove cached data
        
        [self notifyCache];
        [self operationSucceeded];
        
    }
    if (self.response.statusCode >= 300 && self.response.statusCode < 400) {
        
        if(self.response.statusCode == 301) {
            DLog(@"%@ has moved to %@", self.url, [self.response.URL absoluteString]);
        }
        else if(self.response.statusCode == 304) {
            
            for(MKNKVoidBlock notModifiedBlock in self.notModifiedHandlers) {
                
                notModifiedBlock();
            }
        }
        else if(self.response.statusCode == 307) {
            DLog(@"%@ temporarily redirected", self.url);
        }
        else {
            DLog(@"%@ returned status %d", self.url, (int) self.response.statusCode);
        }
        
    } else if (self.response.statusCode >= 400 && self.response.statusCode < 600 && ![self isCancelled]) {
        
        [self operationFailedWithError:[NSError errorWithDomain:NSURLErrorDomain
                                                           code:self.response.statusCode
                                                       userInfo:self.response.allHeaderFields]];
    }
    [self endBackgroundTask];
    
}

#pragma mark -
#pragma mark Our methods to get data

-(NSData*) responseData {
    
    if([self isFinished])
        return self.mutableData;
    else if(self.cachedResponse)
        return self.cachedResponse;
    else
        return nil;
}

-(NSString*)responseString {
    
    return [self responseStringWithEncoding:self.stringEncoding];
}

-(NSString*) responseStringWithEncoding:(NSStringEncoding) encoding {
    
    return [[NSString alloc] initWithData:[self responseData] encoding:encoding];
}

#if TARGET_OS_IPHONE
-(UIImage*) responseImage {
    
    return [UIImage imageWithData:[self responseData]];
}

-(void) decompressedResponseImageOfSize:(CGSize) size completionHandler:(void (^)(UIImage *decompressedImage)) imageDecompressionHandler {
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)([self responseData]), NULL);
        CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, (__bridge CFDictionaryRef)(@{(id)kCGImageSourceShouldCache:@(YES)}));
        UIImage *decompressedImage = [UIImage imageWithCGImage:cgImage];
        if(source)
            CFRelease(source);
        if(cgImage)
            CGImageRelease(cgImage);
        
        dispatch_async(dispatch_get_main_queue(), ^{
            imageDecompressionHandler(decompressedImage);
        });
    });
}

#elif TARGET_OS_MAC
-(NSImage*) responseImage {
    
    return [[NSImage alloc] initWithData:[self responseData]];
}

-(NSXMLDocument*) responseXML {
    
    return [[NSXMLDocument alloc] initWithData:[self responseData] options:0 error:nil];
}
#endif

-(id) responseJSON {
    
    if([self responseData] == nil) return nil;
    NSError *error = nil;
    id returnValue = [NSJSONSerialization JSONObjectWithData:[self responseData] options:0 error:&error];
    if(error) DLog(@"JSON Parsing Error: %@", error);
    return returnValue;
}

-(void) responseJSONWithCompletionHandler:(void (^)(id jsonObject)) jsonDecompressionHandler {
    
    [self responseJSONWithOptions:0 completionHandler:jsonDecompressionHandler];
}

-(void) responseJSONWithOptions:(NSJSONReadingOptions) options completionHandler:(void (^)(id jsonObject)) jsonDecompressionHandler {
    
    if([self responseData] == nil) {
        
        jsonDecompressionHandler(nil);
        return;
    }
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        
        NSError *error = nil;
        id returnValue = [NSJSONSerialization JSONObjectWithData:[self responseData] options:options error:&error];
        if(error) {
            
            DLog(@"JSON Parsing Error: %@", error);
            jsonDecompressionHandler(nil);
            return;
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            jsonDecompressionHandler(returnValue);
        });
    });
}

#pragma mark -
#pragma mark Overridable methods

-(void) operationSucceeded {
    
    for(MKNKResponseBlock responseBlock in self.responseBlocks)
        responseBlock(self);
}

-(void) showLocalNotification {
#if TARGET_OS_IPHONE
    
    if(self.localNotification) {
        
        [[UIApplication sharedApplication] presentLocalNotificationNow:self.localNotification];
    } else if(self.shouldShowLocalNotificationOnError) {
        
        UILocalNotification *localNotification = [[UILocalNotification alloc] init];
        
        localNotification.alertBody = [self.error localizedDescription];
        localNotification.alertAction = NSLocalizedString(@"Dismiss", @"");
        
        [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
    }
#endif
}

-(void) operationFailedWithError:(NSError*) error {
    
    self.error = error;
    DLog(@"%@, [%@]", self, [self.error localizedDescription]);
    for(MKNKErrorBlock errorBlock in self.errorBlocks)
        errorBlock(error);
    
    for(MKNKResponseErrorBlock errorBlock in self.errorBlocksType2)
        errorBlock(self, error);
    
#if TARGET_OS_IPHONE
    if([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground)
        [self showLocalNotification];
#endif
    
}

@end
#endif